import { getLogger } from '@jitsi/logger';
import { Strophe } from 'strophe.js';

import JitsiConference from './JitsiConference';
import * as JitsiConferenceErrors from './JitsiConferenceErrors';
import { JitsiConferenceEvents } from './JitsiConferenceEvents';
import { JitsiTrackEvents } from './JitsiTrackEvents';
import JitsiRemoteTrack from './modules/RTC/JitsiRemoteTrack';
import TraceablePeerConnection from './modules/RTC/TraceablePeerConnection';
import RTCStats from './modules/RTCStats/RTCStats';
import { RTCStatsEvents } from './modules/RTCStats/RTCStatsEvents';
import JibriSession from './modules/recording/JibriSession';
import { SPEAKERS_AUDIO_LEVELS } from './modules/statistics/constants';
import Statistics from './modules/statistics/statistics';
import EventEmitterForwarder from './modules/util/EventEmitterForwarder';
import JingleSessionPC from './modules/xmpp/JingleSessionPC';
import { MediaType } from './service/RTC/MediaType';
import { RTCEvents } from './service/RTC/RTCEvents';
import { VideoType } from './service/RTC/VideoType';
import { AuthenticationEvents } from './service/authentication/AuthenticationEvents';
import {
    AnalyticsEvents,
    createBridgeDownEvent,
    createConnectionStageReachedEvent,
    createFocusLeftEvent,
    createJingleEvent,
    createRemotelyMutedEvent
} from './service/statistics/AnalyticsEvents';
import { XMPPEvents } from './service/xmpp/XMPPEvents';

const logger = getLogger('core:JitsiConferenceEventManager');

/**
 * Setups all event listeners related to conference
 */
export default class JitsiConferenceEventManager {
    private conference: JitsiConference;
    private xmppListeners: { [key: string]: (...args: any[]) => void; }; // Todo
    private chatRoomForwarder?: EventEmitterForwarder;

    /**
     * Setups all event listeners related to conference
     * @param conference {JitsiConference} the conference
     */
    constructor(conference: JitsiConference) {
        this.conference = conference;
        this.xmppListeners = {};
    }

    /**
     * Add XMPP listener and save its reference for remove on leave conference.
     * @param {string} eventName - The event name.
     * @param {Function} listener - The listener function.
     * @private
     */
    private _addConferenceXMPPListener(eventName: string, listener: (...args: any[]) => void): void { // Todo
        this.xmppListeners[eventName] = listener;
        this.conference.xmpp.addListener(eventName, listener);
    }

    /**
     * Setups event listeners related to conference.chatRoom
     */
    setupChatRoomListeners(): void {
        const conference = this.conference;
        const chatRoom = conference.room;

        this.chatRoomForwarder = new EventEmitterForwarder(chatRoom,
            this.conference.eventEmitter);

        chatRoom.addListener(XMPPEvents.PARTICIPANT_FEATURES_CHANGED, (from: string, features: Set<string>) => {
            const participant = conference.getParticipantById(Strophe.getResourceFromJid(from));

            if (participant) {
                participant.setFeatures(features);
                conference.eventEmitter.emit(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, participant);
            }
        });

        this.chatRoomForwarder.forward(XMPPEvents.PERMISSIONS_RECEIVED, JitsiConferenceEvents.PERMISSIONS_RECEIVED);

        chatRoom.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS,
            (actor: string) => {
                // TODO: Add a way to differentiate between commands which caused
                // us to mute and those that did not change our state (i.e. we were
                // already muted).
                Statistics.sendAnalytics(createRemotelyMutedEvent(MediaType.AUDIO));

                conference.mutedByFocusActor = actor;

                // set isMutedByFocus when setAudioMute Promise ends
                conference.rtc.setAudioMute().then(
                    () => {
                        conference.isMutedByFocus = true;
                        conference.mutedByFocusActor = null;
                    })
                    .catch(
                        (error: Error) => {
                            conference.mutedByFocusActor = null;
                            logger.warn(
                                'Error while audio muting due to focus request', error);
                        });
            }
        );

        chatRoom.addListener(XMPPEvents.VIDEO_MUTED_BY_FOCUS,
            (actor: string) => {
                // TODO: Add a way to differentiate between commands which caused
                // us to mute and those that did not change our state (i.e. we were
                // already muted).
                Statistics.sendAnalytics(createRemotelyMutedEvent(MediaType.VIDEO));

                conference.mutedVideoByFocusActor = actor;

                // set isVideoMutedByFocus when setVideoMute Promise ends
                conference.rtc.setVideoMute().then(
                    () => {
                        conference.isVideoMutedByFocus = true;
                        conference.mutedVideoByFocusActor = null;
                    })
                    .catch(
                        (error: Error) => {
                            conference.mutedVideoByFocusActor = null;
                            logger.warn(
                                'Error while video muting due to focus request', error);
                        });
            }
        );

        chatRoom.addListener(XMPPEvents.DESKTOP_MUTED_BY_FOCUS,
            (actor: string) => {
                // TODO: Add a way to differentiate between commands which caused
                // us to mute and those that did not change our state (i.e. we were
                // already muted).
                Statistics.sendAnalytics(createRemotelyMutedEvent(MediaType.DESKTOP));

                conference.mutedDesktopByFocusActor = actor;

                // set isDesktopMutedByFocus when setDesktopMute Promise ends
                conference.rtc.setDesktopMute().then(
                    () => {
                        conference.isDesktopMutedByFocus = true;
                        conference.mutedDesktopByFocusActor = null;
                    })
                    .catch(
                        (error: Error) => {
                            conference.mutedDesktopByFocusActor = null;
                            logger.warn(
                                'Error while desktop video muting due to focus request', error);
                        });
            }
        );

        this.chatRoomForwarder.forward(XMPPEvents.SUBJECT_CHANGED,
            JitsiConferenceEvents.SUBJECT_CHANGED);

        this.chatRoomForwarder.forward(XMPPEvents.MUC_JOINED,
            JitsiConferenceEvents.CONFERENCE_JOINED);

        this.chatRoomForwarder.forward(XMPPEvents.MUC_JOIN_IN_PROGRESS,
            JitsiConferenceEvents.CONFERENCE_JOIN_IN_PROGRESS);

        this.chatRoomForwarder.forward(XMPPEvents.MEETING_ID_SET,
            JitsiConferenceEvents.CONFERENCE_UNIQUE_ID_SET);

        this.chatRoomForwarder.forward(XMPPEvents.CONFERENCE_TIMESTAMP_RECEIVED,
            JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP);

        // send some analytics events
        chatRoom.addListener(XMPPEvents.MUC_JOINED,
            () => {
                this.conference._onMucJoined();

                this.conference.isJvbConnectionInterrupted = false;

                // TODO: Move all of the 'connectionTimes' logic to its own module.
                Object.keys(chatRoom.connectionTimes).forEach(key => {
                    const event
                        = createConnectionStageReachedEvent(
                            `conference_${key}`,
                            { value: chatRoom.connectionTimes[key] });

                    Statistics.sendAnalytics(event);
                });

                // TODO: Move all of the 'connectionTimes' logic to its own module.
                Object.keys(chatRoom.xmpp.connectionTimes).forEach(key => {
                    const event
                        = createConnectionStageReachedEvent(
                            `xmpp_${key}`,
                            { value: chatRoom.xmpp.connectionTimes[key] });

                    Statistics.sendAnalytics(event);
                });
            });

        chatRoom.addListener(JitsiTrackEvents.TRACK_OWNER_SET, (track: JitsiRemoteTrack, owner: string, sourceName: string, videoType: VideoType) => {
            if (track.getParticipantId() !== owner || track.getSourceName() !== sourceName) {
                conference.eventEmitter.emit(JitsiConferenceEvents.TRACK_REMOVED, track);

                // Update the owner and other properties on the track.
                track.setOwner(owner);
                track.setSourceName(sourceName);
                track._setVideoType(videoType);
                owner && conference.eventEmitter.emit(JitsiConferenceEvents.TRACK_ADDED, track);
            }
        });

        this.chatRoomForwarder.forward(XMPPEvents.ROOM_JOIN_ERROR,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.CONNECTION_ERROR);

        this.chatRoomForwarder.forward(XMPPEvents.DISPLAY_NAME_REQUIRED,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.DISPLAY_NAME_REQUIRED);

        this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_ERROR,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.CONNECTION_ERROR);
        this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_NOT_ALLOWED_ERROR,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.NOT_ALLOWED_ERROR);
        this.chatRoomForwarder.forward(XMPPEvents.ROOM_CONNECT_MEMBERS_ONLY_ERROR,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.MEMBERS_ONLY_ERROR);

        this.chatRoomForwarder.forward(XMPPEvents.ROOM_MAX_USERS_ERROR,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.CONFERENCE_MAX_USERS);

        this.chatRoomForwarder.forward(XMPPEvents.PASSWORD_REQUIRED,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.PASSWORD_REQUIRED);

        this.chatRoomForwarder.forward(XMPPEvents.AUTHENTICATION_REQUIRED,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.AUTHENTICATION_REQUIRED);

        this.chatRoomForwarder.forward(XMPPEvents.BRIDGE_DOWN,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE);
        chatRoom.addListener(
            XMPPEvents.BRIDGE_DOWN,
            () => Statistics.sendAnalytics(createBridgeDownEvent()));

        this.chatRoomForwarder.forward(XMPPEvents.RESERVATION_ERROR,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.RESERVATION_ERROR);

        this.chatRoomForwarder.forward(XMPPEvents.GRACEFUL_SHUTDOWN,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.GRACEFUL_SHUTDOWN);

        this.chatRoomForwarder.forward(XMPPEvents.MUC_DESTROYED,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.CONFERENCE_DESTROYED);

        this.chatRoomForwarder.forward(XMPPEvents.CHAT_ERROR_RECEIVED,
            JitsiConferenceEvents.CONFERENCE_ERROR,
            JitsiConferenceErrors.CHAT_ERROR);

        this.chatRoomForwarder.forward(XMPPEvents.SETTINGS_ERROR_RECEIVED,
            JitsiConferenceEvents.CONFERENCE_ERROR,
            JitsiConferenceErrors.SETTINGS_ERROR);

        this.chatRoomForwarder.forward(XMPPEvents.FOCUS_DISCONNECTED,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.FOCUS_DISCONNECTED);

        chatRoom.addListener(XMPPEvents.FOCUS_LEFT,
            () => {
                Statistics.sendAnalytics(createFocusLeftEvent());
                conference.eventEmitter.emit(
                    JitsiConferenceEvents.CONFERENCE_FAILED,
                    JitsiConferenceErrors.FOCUS_LEFT);
            });

        chatRoom.addListener(XMPPEvents.SESSION_ACCEPT_TIMEOUT,
            (jingleSession: JingleSessionPC) => {
                Statistics.sendAnalyticsAndLog(
                    createJingleEvent(
                        AnalyticsEvents.ACTION_JINGLE_SA_TIMEOUT,
                        { p2p: jingleSession.isP2P }));
            });

        chatRoom.addListener(XMPPEvents.RECORDER_STATE_CHANGED,
            (session: JibriSession, jid: string) => {

                if (jid) {
                    const resource = Strophe.getResourceFromJid(jid);
                    const participant = conference.getParticipantById(resource) || resource;

                    if (session.getStatus() === 'off') {
                        session.setTerminator(participant);
                    } else if (session.getStatus() === 'on') {
                        session.setInitiator(participant);
                    }
                }

                conference.eventEmitter.emit(
                    JitsiConferenceEvents.RECORDER_STATE_CHANGED,
                    session);
            });

        this.chatRoomForwarder.forward(XMPPEvents.TRANSCRIPTION_STATUS_CHANGED,
            JitsiConferenceEvents.TRANSCRIPTION_STATUS_CHANGED);

        this.chatRoomForwarder.forward(XMPPEvents.VIDEO_SIP_GW_AVAILABILITY_CHANGED,
            JitsiConferenceEvents.VIDEO_SIP_GW_AVAILABILITY_CHANGED);

        this.chatRoomForwarder.forward(
            XMPPEvents.VIDEO_SIP_GW_SESSION_STATE_CHANGED,
            JitsiConferenceEvents.VIDEO_SIP_GW_SESSION_STATE_CHANGED);

        this.chatRoomForwarder.forward(XMPPEvents.PHONE_NUMBER_CHANGED,
            JitsiConferenceEvents.PHONE_NUMBER_CHANGED);

        chatRoom.setParticipantPropertyListener((id: string, prop: string, value: string) => {
            const participant = conference.getParticipantById(id);

            if (!participant) {
                return;
            }

            participant.setProperty(prop, value);
        });

        chatRoom.addListener(XMPPEvents.KICKED,
            conference.onMemberKicked.bind(conference));
        chatRoom.addListener(XMPPEvents.SUSPEND_DETECTED,
            conference.onSuspendDetected.bind(conference));

        this.chatRoomForwarder.forward(XMPPEvents.MUC_LOCK_CHANGED,
            JitsiConferenceEvents.LOCK_STATE_CHANGED);

        this.chatRoomForwarder.forward(XMPPEvents.MUC_MEMBERS_ONLY_CHANGED,
            JitsiConferenceEvents.MEMBERS_ONLY_CHANGED);
        this.chatRoomForwarder.forward(XMPPEvents.MUC_VISITORS_SUPPORTED_CHANGED,
            JitsiConferenceEvents.VISITORS_SUPPORTED_CHANGED);

        chatRoom.addListener(XMPPEvents.MUC_MEMBER_JOINED,
            conference.onMemberJoined.bind(conference));
        this.chatRoomForwarder.forward(XMPPEvents.MUC_LOBBY_MEMBER_JOINED,
            JitsiConferenceEvents.LOBBY_USER_JOINED);
        this.chatRoomForwarder.forward(XMPPEvents.MUC_LOBBY_MEMBER_UPDATED,
            JitsiConferenceEvents.LOBBY_USER_UPDATED);
        this.chatRoomForwarder.forward(XMPPEvents.MUC_LOBBY_MEMBER_LEFT,
            JitsiConferenceEvents.LOBBY_USER_LEFT);
        chatRoom.addListener(XMPPEvents.MUC_MEMBER_BOT_TYPE_CHANGED,
            conference._onMemberBotTypeChanged.bind(conference));
        chatRoom.addListener(XMPPEvents.MUC_MEMBER_LEFT,
            conference.onMemberLeft.bind(conference));
        this.chatRoomForwarder.forward(XMPPEvents.MUC_LEFT,
            JitsiConferenceEvents.CONFERENCE_LEFT);
        this.chatRoomForwarder.forward(XMPPEvents.MUC_DENIED_ACCESS,
            JitsiConferenceEvents.CONFERENCE_FAILED,
            JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED);

        chatRoom.addListener(XMPPEvents.DISPLAY_NAME_CHANGED,
            conference.onDisplayNameChanged.bind(conference));

        chatRoom.addListener(XMPPEvents.SILENT_STATUS_CHANGED,
            conference.onSilentStatusChanged.bind(conference));

        chatRoom.addListener(XMPPEvents.LOCAL_ROLE_CHANGED, (role: string) => {
            conference.onLocalRoleChanged(role);
        });

        chatRoom.addListener(XMPPEvents.MUC_ROLE_CHANGED,
            conference.onUserRoleChanged.bind(conference));

        chatRoom.addListener(AuthenticationEvents.IDENTITY_UPDATED,
            (authEnabled: boolean, authIdentity: any) => {
                conference.authEnabled = authEnabled;
                conference.authIdentity = authIdentity;
                conference.eventEmitter.emit(
                    JitsiConferenceEvents.AUTH_STATUS_CHANGED, authEnabled,
                    authIdentity);
            });

        chatRoom.addListener(
            XMPPEvents.MESSAGE_RECEIVED,

            // eslint-disable-next-line max-params
            (jid: string, txt: string, myJid: string, ts: number,
                    displayName: string, isVisitor: boolean, messageId: string, source: string, replyToId?: string) => {
                const participantId = Strophe.getResourceFromJid(jid);

                conference.eventEmitter.emit(
                    JitsiConferenceEvents.MESSAGE_RECEIVED,
                    participantId, txt, ts, displayName, isVisitor, messageId, source, replyToId);
            });

        this.chatRoomForwarder.forward(XMPPEvents.POLLS_RECEIVE_EVENT,
            JitsiConferenceEvents.POLL_RECEIVED);
        this.chatRoomForwarder.forward(XMPPEvents.POLLS_ANSWER_EVENT,
            JitsiConferenceEvents.POLL_ANSWER_RECEIVED);

        chatRoom.addListener(
            XMPPEvents.REACTION_RECEIVED,

            (jid: string, reactionList: string[], messageId: string) => {
                const participantId = Strophe.getResourceFromJid(jid);

                conference.eventEmitter.emit(
                    JitsiConferenceEvents.REACTION_RECEIVED,
                    participantId, reactionList, messageId);
            });

        chatRoom.addListener(
            XMPPEvents.PRIVATE_MESSAGE_RECEIVED,

            // eslint-disable-next-line max-params
            (jid: string, txt: string, myJid: string, ts: number, messageId: string,
                    displayName: string, isVisitor: boolean, ofrom: string, replyToId?: string) => {
                const participantId = isVisitor ? ofrom : Strophe.getResourceFromJid(jid);

                conference.eventEmitter.emit(
                    JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
                    participantId, txt, ts, messageId, displayName, isVisitor, replyToId);
            });

        chatRoom.addListener(XMPPEvents.PRESENCE_STATUS,
            (jid: string, status: string) => {
                const id = Strophe.getResourceFromJid(jid);
                const participant = conference.getParticipantById(id);

                if (!participant || participant._status === status) {
                    return;
                }
                participant._status = status;
                conference.eventEmitter.emit(
                    JitsiConferenceEvents.USER_STATUS_CHANGED, id, status);
            });

        chatRoom.addListener(XMPPEvents.JSON_MESSAGE_RECEIVED,
            (from: string, payload: any) => {
                const id = Strophe.getResourceFromJid(from);
                const participant = conference.getParticipantById(id);

                if (participant) {
                    conference.eventEmitter.emit(
                        JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
                        participant, payload);
                } else {
                    conference.eventEmitter.emit(
                        JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
                        id, payload);
                }
            });

        // Breakout rooms.
        this.chatRoomForwarder.forward(XMPPEvents.BREAKOUT_ROOMS_MOVE_TO_ROOM,
            JitsiConferenceEvents.BREAKOUT_ROOMS_MOVE_TO_ROOM);
        this.chatRoomForwarder.forward(XMPPEvents.BREAKOUT_ROOMS_UPDATED,
            JitsiConferenceEvents.BREAKOUT_ROOMS_UPDATED);

        // File sharing
        this.chatRoomForwarder.forward(XMPPEvents.FILE_SHARING_FILES_RECEIVED,
            JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED);
        this.chatRoomForwarder.forward(XMPPEvents.FILE_SHARING_FILE_ADDED,
            JitsiConferenceEvents.FILE_SHARING_FILE_ADDED);
        this.chatRoomForwarder.forward(XMPPEvents.FILE_SHARING_FILE_REMOVED,
            JitsiConferenceEvents.FILE_SHARING_FILE_REMOVED);

        // Room metadata.
        chatRoom.addListener(XMPPEvents.ROOM_METADATA_UPDATED, (metadata: any) => {
            if (metadata.startMuted) {
                const audio = metadata.startMuted.audio || false;
                const video = metadata.startMuted.video || false;

                audio && (conference.isMutedByFocus = true);
                video && (conference.isVideoMutedByFocus = true);
                conference._updateStartMutedPolicy(audio, video);
            }
            if (metadata.recording && typeof metadata.recording.isTranscribingEnabled !== 'undefined') {
                conference._setTranscribingEnabled(Boolean(metadata.recording.isTranscribingEnabled));
            }
            conference.eventEmitter.emit(JitsiConferenceEvents.METADATA_UPDATED, metadata);
        });
    }

    /**
     * Setups event listeners related to conference.rtc
     */
    setupRTCListeners(): void {
        const conference = this.conference;
        const rtc = conference.rtc;

        rtc.addListener(
            RTCEvents.REMOTE_TRACK_ADDED,
            conference.onRemoteTrackAdded.bind(conference));

        rtc.addListener(
            RTCEvents.REMOTE_TRACK_REMOVED,
            conference.onRemoteTrackRemoved.bind(conference));

        rtc.addListener(RTCEvents.DOMINANT_SPEAKER_CHANGED,
            (dominant: string, previous: string[], silence: boolean) => {
                if ((conference.lastDominantSpeaker !== dominant || conference.dominantSpeakerIsSilent !== silence)
                        && conference.room) {
                    conference.dominantSpeakerIsSilent = silence;
                    conference.eventEmitter.emit(
                        JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, dominant, previous, silence);
                    if (conference.statistics && conference.myUserId() === dominant) {
                        // We are the new dominant speaker.
                        conference.xmpp.sendDominantSpeakerEvent(conference.room.roomjid, silence);
                        RTCStats.sendStatsEntry(RTCStatsEvents.DOMINANT_SPEAKER_CHANGED_EVENT);
                    }
                    if (conference.lastDominantSpeaker !== dominant) {
                        conference.lastDominantSpeaker = dominant;

                        if (previous?.length) {
                            const speakerList = previous.slice(0);

                            // Add the dominant speaker to the top of the list (exclude self).
                            if (conference.myUserId() !== dominant) {
                                speakerList.splice(0, 0, dominant);
                            }

                            // Trim the list to the top 5 speakers only.
                            if (speakerList.length > SPEAKERS_AUDIO_LEVELS) {
                                speakerList.splice(SPEAKERS_AUDIO_LEVELS, speakerList.length - SPEAKERS_AUDIO_LEVELS);
                            }
                            conference.statistics && conference.statistics.setSpeakerList(speakerList);
                        }
                    }
                }
            });

        rtc.addListener(RTCEvents.DATA_CHANNEL_OPEN, () => {
            const now = window.performance.now();
            const key = 'data.channel.opened';

            // TODO: Move all of the 'connectionTimes' logic to its own module.
            logger.info(`(TIME) ${key}:\t`, now);
            conference.room.connectionTimes[key] = now;
            Statistics.sendAnalytics(
                createConnectionStageReachedEvent(key, { value: now }));

            conference.eventEmitter.emit(JitsiConferenceEvents.DATA_CHANNEL_OPENED);
        });

        rtc.addListener(RTCEvents.DATA_CHANNEL_CLOSED, (ev: RTCDataChannelEvent) => {
            conference.eventEmitter.emit(JitsiConferenceEvents.DATA_CHANNEL_CLOSED, ev);
        });

        rtc.addListener(RTCEvents.VIDEO_SSRCS_REMAPPED, (msg: any) => {
            this.conference.jvbJingleSession.processSourceMap(msg, MediaType.VIDEO);
        });

        rtc.addListener(RTCEvents.AUDIO_SSRCS_REMAPPED, (msg: any) => {
            this.conference.jvbJingleSession.processSourceMap(msg, MediaType.AUDIO);
        });

        rtc.addListener(RTCEvents.BRIDGE_BWE_STATS_RECEIVED, (bwe: any) => {
            conference.eventEmitter.emit(JitsiConferenceEvents.BRIDGE_BWE_STATS_RECEIVED, bwe);
        });

        rtc.addListener(RTCEvents.ENDPOINT_MESSAGE_RECEIVED,
            (from: string, payload: any) => {
                if (from === 'transcriber') {
                    conference.eventEmitter.emit(
                        JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
                        from, payload);

                    return;
                }

                const participant = conference.getParticipantById(from);

                if (participant) {
                    conference.eventEmitter.emit(
                        JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
                        participant, payload);
                } else {
                    logger.warn(
                        'Ignored ENDPOINT_MESSAGE_RECEIVED for not existing '
                            + `participant: ${from}`,
                        payload);
                }
            });

        rtc.addListener(RTCEvents.ENDPOINT_STATS_RECEIVED,
            (from: string, payload: any) => {
                const participant = conference.getParticipantById(from);

                if (participant) {
                    conference.eventEmitter.emit(JitsiConferenceEvents.ENDPOINT_STATS_RECEIVED, participant, payload);
                } else {
                    logger.warn(`Ignoring ENDPOINT_STATS_RECEIVED for a non-existant participant: ${from}`);
                }
            });
    }

    /**
     * Setups event listeners related to conference.statistics
     */
    setupStatisticsListeners(): void {
        const conference = this.conference;

        if (!conference.statistics) {
            return;
        }

        /* eslint-disable max-params */
        conference.statistics.addAudioLevelListener((tpc: TraceablePeerConnection, ssrc: number, level: number, isLocal: boolean) => {
            conference.rtc.setAudioLevel(tpc, ssrc, level, isLocal);
        });

        /* eslint-enable max-params */

        // Forward the "before stats disposed" event
        conference.statistics.addBeforeDisposedListener(() => {
            conference.eventEmitter.emit(
                JitsiConferenceEvents.BEFORE_STATISTICS_DISPOSED);
        });

        conference.statistics.addEncodeTimeStatsListener((tpc: TraceablePeerConnection, stats: RTCEncodedAudioFrameMetadata) => {
            conference.eventEmitter.emit(
                JitsiConferenceEvents.ENCODE_TIME_STATS_RECEIVED, tpc, stats);
        });

        // if we are in startSilent mode we will not be sending/receiving so nothing to detect
        if (!conference.options.config.startSilent) {
            conference.statistics.addByteSentStatsListener((tpc: TraceablePeerConnection, stats: RTCEncodedAudioFrameMetadata) => {
                conference.getLocalTracks(MediaType.AUDIO).forEach(track => {
                    const ssrc = tpc.getLocalSSRC(track);

                    if (!ssrc || !stats.hasOwnProperty(ssrc)) {
                        return;
                    }

                    track.onByteSentStatsReceived(tpc, stats[ssrc]);
                });
            });
        }
    }

    /**
     * Setups event listeners related to conference.xmpp
     */
    setupXMPPListeners(): void {
        const conference = this.conference;

        this._addConferenceXMPPListener(
            XMPPEvents.CALL_INCOMING,
            conference.onIncomingCall.bind(conference));
        this._addConferenceXMPPListener(
            XMPPEvents.CALL_ACCEPTED,
            conference.onCallAccepted.bind(conference));
        this._addConferenceXMPPListener(
            XMPPEvents.TRANSPORT_INFO,
            conference.onTransportInfo.bind(conference));
        this._addConferenceXMPPListener(
            XMPPEvents.CALL_ENDED,
            conference.onCallEnded.bind(conference));

        this._addConferenceXMPPListener(XMPPEvents.AV_MODERATION_CHANGED,
            (value: boolean, mediaType: MediaType, actorJid: string) => {
                const actorParticipant = conference.getParticipants().find(p => p.getJid() === actorJid);

                conference.eventEmitter.emit(JitsiConferenceEvents.AV_MODERATION_CHANGED, {
                    actor: actorParticipant,
                    enabled: value,
                    mediaType
                });
            });
        this._addConferenceXMPPListener(XMPPEvents.AV_MODERATION_PARTICIPANT_APPROVED,
            (mediaType: MediaType, jid: string) => {
                const participant = conference.getParticipantById(Strophe.getResourceFromJid(jid));

                if (participant) {
                    conference.eventEmitter.emit(JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_APPROVED, {
                        mediaType,
                        participant
                    });
                }
            });
        this._addConferenceXMPPListener(XMPPEvents.AV_MODERATION_PARTICIPANT_REJECTED,
            (mediaType: MediaType, jid: string) => {
                const participant = conference.getParticipantById(Strophe.getResourceFromJid(jid));

                if (participant) {
                    conference.eventEmitter.emit(JitsiConferenceEvents.AV_MODERATION_PARTICIPANT_REJECTED, {
                        mediaType,
                        participant
                    });
                }
            });
        this._addConferenceXMPPListener(XMPPEvents.AV_MODERATION_APPROVED,
            (value: MediaType) => conference.eventEmitter.emit(JitsiConferenceEvents.AV_MODERATION_APPROVED, { mediaType: value }));
        this._addConferenceXMPPListener(XMPPEvents.AV_MODERATION_REJECTED,
            (value: MediaType) => {
                conference.eventEmitter.emit(JitsiConferenceEvents.AV_MODERATION_REJECTED, { mediaType: value });
            });

        this._addConferenceXMPPListener(XMPPEvents.VISITORS_MESSAGE,
            (value: string) => conference.eventEmitter.emit(JitsiConferenceEvents.VISITORS_MESSAGE, value));
        this._addConferenceXMPPListener(XMPPEvents.VISITORS_REJECTION,
            () => conference.eventEmitter.emit(JitsiConferenceEvents.VISITORS_REJECTION));
    }


    /**
     * Removes event listeners related to conference.xmpp
     */
    removeXMPPListeners(): void {
        const conference = this.conference;

        Object.keys(this.xmppListeners).forEach(eventName => {
            conference.xmpp.removeListener(
                eventName,
                this.xmppListeners[eventName]);
        });
        this.xmppListeners = {};
    }
}
